package com.tukez.tmdb
import java.text.SimpleDateFormat
import org.apache.http.client.utils.URLEncodedUtils
import org.apache.http.message.BasicNameValuePair
import org.apache.http.HttpStatus
import org.joda.time.ReadableInstant
import com.tukez.tmdb.http.HttpClient
import com.tukez.tmdb.http.IHttpClient
import com.tukez.tmdb.model.Genre
import com.tukez.tmdb.model.Image
import com.tukez.tmdb.model.Movie
import com.tukez.tmdb.model.MovieDetails
import com.tukez.tmdb.model.Person
import com.tukez.tmdb.model.PersonDetails
import com.tukez.tmdb.query.GenresSelector
import com.tukez.tmdb.query.MPAARating
import com.tukez.tmdb.query.Order
import com.tukez.tmdb.query.OrderBy
import com.tukez.tmdb.query.Paging
import com.tukez.tmdb.query.RatingBetween
import com.tukez.tmdb.query.ReleaseBetween
import com.tukez.tmdb.util.DisjointBoundedView._
import com.tukez.tmdb.util.DateTimeSerializer
import com.tukez.tmdb.util.DurationSerializer
import com.tukez.tmdb.util.LocalDateSerializer
import net.liftweb.json.JsonAST.JArray
import net.liftweb.json.JsonAST.JField
import net.liftweb.json.JsonAST.JObject
import net.liftweb.json.JsonAST.JValue
import net.liftweb.json.JsonAST.JNothing
import net.liftweb.json.CustomSerializer
import net.liftweb.json.Extraction
import net.liftweb.json.MappingException
import net.liftweb.json
import net.liftweb.json.JsonAST.JString
import com.tukez.tmdb.model.Translation
import com.tukez.tmdb.model.MovieVersion
import com.tukez.tmdb.model.Images
import com.tukez.tmdb.model.MovieOptional
import java.net.URLEncoder
import com.tukez.tmdb.model.PersonVersion
import com.tukez.tmdb.http.RequestLimits
import com.tukez.tmdb.model.ErrorDetails
import com.tukez.tmdb.query.Hash

object TMDb {
  def apply(apikey: String, language: String = "en", http: IHttpClient = new HttpClient with RequestLimits) = new TMDb(apikey, language, http)
}

class TMDb(private val apikey: String, val language: String = "en", private val http: IHttpClient = new HttpClient with RequestLimits) extends ITMDb {

  private val baseUrl = "http://api.themoviedb.org"
  private val apiVersion = "2.1"
  private val apiType = "json"

  @volatile private var genreIds: Option[Map[String, Long]] = None

  private implicit val jsonFormats = new json.DefaultFormats {
    override def dateFormatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
  } + DateTimeSerializer + LocalDateSerializer + DurationSerializer

  override object Media extends ITMDbMedia {

    override def getInfo(dvdId: String): Option[Movie] = {
      getInfoInternal(dvdId)
    }

    override def getInfo(hash: Hash): Option[Movie] = {
      getInfoInternal(hash.hash + "/" + hash.bytesize)
    }

    private def getInfoInternal(urlEnding: String) = {
      val response = httpGetJson(urlFor("Media.getInfo") + "/" + urlEnding)
      val movieJson = parseMovieFromJson(response)
      movieJson.extract[Option[Movie]]
    }
  }
  
  override object Movie extends ITMDbMovie {

    override def browse[A <% GenresType, B <% CertsType](
      orderBy: OrderBy.OrderBy,
      order: Order.Order,
      paging: Paging = null,
      query: String = "",
      minVotes: Int = -1,
      rating: RatingBetween = null,
      genres: A = null,
      release: ReleaseBetween = null,
      year: Int = -1,
      certifications: List[B] = Nil,
      companies: List[String] = Nil,
      countries: List[String] = Nil): List[Movie] = {

      // Build URL with parameters

      val url = urlFor("Movie.browse")
      val params = scala.collection.mutable.Map[String, Any]()

      def genreIdForName(genreName: String): Long = {
        genreIds match {
          case Some(genres) => genres.getOrElse(genreName, throw new IllegalArgumentException("""Genre """" + genreName + """" not found"""))
          case None => {
            Genres.getList // Caches the genres
            genreIds.get.getOrElse(genreName, throw new IllegalArgumentException("""Genre """" + genreName + """" not found"""))
          }
        }
      }
      def addOptionalList(name: String, values: List[Any]) {
        if (values != null) values match {
          case Nil => // Do nothing
          case head :: Nil => params.put(name, head)
          case list => params.put(name, list.mkString(","))
        }
      }

      params.put("order_by", orderBy.toString.toLowerCase)
      params.put("order", order.toString.toLowerCase)

      if (paging != null) {
        params.put("per_page", paging.perPage)
        params.put("page", paging.page)
      }

      if (!query.trim.isEmpty) params.put("query", query.trim)
      if (minVotes >= 0) params.put("min_votes", minVotes)

      if (rating != null) {
        params.put("rating_min", rating.min)
        params.put("rating_max", rating.max)
      }

      if (genres != null) {
        genres match {
          case genreSelector: GenresSelector[_] => {
            val genreIds = for (x <- genreSelector.genres) yield x match {
              case genreId: Int => genreId
              case genreId: Long => genreId
              case genreName: String => genreIdForName(genreName)
              case genre: com.tukez.tmdb.query.Genres.Genre => genre.genreId
              case genre: Genre => genre.id
            }
            genreIds match {
              case Nil => // Do nothing
              case head :: Nil => params.put("genres", head)
              case list => {
                params.put("genres", list.mkString(","))
                params.put("genres_selector", genreSelector.selector)
              }
            }
          }
          case genreId: Int => params.put("genres", genreId)
          case genreId: Long => params.put("genres", genreId)
          case genreName: String => params.put("genres", genreIdForName(genreName))
          case genre: com.tukez.tmdb.query.Genres.Genre => params.put("genres", genre.genreId)
          case genre: Genre => params.put("genres", genre.id)
        }
      }

      if (release != null) {
        params.put("release_min", release.min.getMillis)
        params.put("release_max", release.max.getMillis)
      }
      if (year >= 0) params.put("year", year)

      addOptionalList("certifications", certifications);
      addOptionalList("companies", companies);

      if (!countries.forall(_.length == 2)) {
        throw new IllegalArgumentException("countries must be 2 letter country codes, was " + countries)
      }
      addOptionalList("countries", countries);

      val paramIterable = for ((name, value) <- params) yield new BasicNameValuePair(name, value.toString)
      import scala.collection.JavaConversions._
      val finalUrl = url + "?" + URLEncodedUtils.format(paramIterable.toList.sortWith(_.getName < _.getName), "UTF-8")

      // Query TMDb
      val response = httpGetJson(finalUrl)
      val movies = parseMovieFromJson(response)
      movies.extract[List[Movie]]
    }

    override def getImages[A <% Int or Long or String](tmdbIdOrImdbId: A): Images = {
      val response = httpGetJson(urlFor("Movie.getImages") + "/" + tmdbIdOrImdbId.toString)
      response match {
        case JNothing => Images(Nil, Nil)
        case _ => {
          val simplifiedJson = simplifyArrays(response, List("posters", "backdrops"))
          val posters = (simplifiedJson \\ "posters").extract[List[Image]]
          val backdrops = (simplifiedJson \\ "backdrops").extract[List[Image]]
          Images(posters, backdrops)
        }
      }
    }

    override def getInfo(tmdbId: Long): Option[Movie] = {
      val response = httpGetJson(urlFor("Movie.getInfo") + "/" + tmdbId)
      val movieJson = parseMovieFromJson(response)
      movieJson.extract[Option[Movie]]
    }

    override def getLatest: MovieVersion = {
      val response = httpGetJson(urlFor("Movie.getLatest"))
      response.extract[Option[MovieVersion]].getOrElse(throw new TMDbParseException("Did not receive latest movie correctly"))
    }

    override def getTranslations[A <% Int or Long or String](tmdbIdOrImdbId: A): List[Translation] = {
      val response = httpGetJson(urlFor("Movie.getTranslations") + "/" + tmdbIdOrImdbId.toString)
      response match {
        case JNothing => Nil
        case _ => (response \\ "translations").extract[List[Translation]]
      }
    }

    override def getVersion[A <% Int or Long or String](tmdbIdsOrImdbIds: A*): List[MovieVersion] = {
      if (tmdbIdsOrImdbIds.isEmpty) throw new IllegalArgumentException("Empty argument list is not allowed")

      val response = httpGetJson(urlFor("Movie.getVersion") + "/" + tmdbIdsOrImdbIds.mkString("%2C"))
      response.extract[List[MovieVersion]]
    }

    override def imdbLookup(imdbId: String): Option[Movie] = {
      val response = httpGetJson(urlFor("Movie.imdbLookup") + "/" + imdbId)
      val movieJson = parseMovieFromJson(response)
      movieJson.extract[Option[Movie]]
    }

    override def search(title: String, year: Int = -1): List[Movie] = {
      val param = if (year >= 0) {
        URLEncoder.encode(title + " " + year, "UTF-8")
      } else {
        URLEncoder.encode(title, "UTF-8")
      }

      val response = httpGetJson(urlFor("Movie.search") + "/" + param)
      val movies = parseMovieFromJson(response)
      movies.extract[List[Movie]]
    }
  }

  override object Person extends ITMDbPerson {

    override def getInfo(tmdbId: Long): Option[Person] = {
      val response = httpGetJson(urlFor("Person.getInfo") + "/" + tmdbId)
      val personJson = parsePersonFromJson(response)
      personJson.extract[Option[Person]]
    }

    override def getLatest: PersonVersion = {
      val response = httpGetJson(urlFor("Person.getLatest"))
      response.extract[Option[PersonVersion]].getOrElse(throw new TMDbParseException("Did not receive latest person correctly"))
    }

    override def getVersion(tmdbIds: Long*): List[PersonVersion] = {
      if (tmdbIds.isEmpty) throw new IllegalArgumentException("Empty argument list is not allowed")

      val response = httpGetJson(urlFor("Person.getVersion") + "/" + tmdbIds.mkString("%2C"))
      response.extract[List[PersonVersion]]
    }

    override def search(title: String): List[Person] = {
      val param = URLEncoder.encode(title, "UTF-8")

      val response = httpGetJson(urlFor("Person.search") + "/" + param)
      val persons = parsePersonFromJson(response)
      persons.extract[List[Person]]
    }

    private def parsePersonFromJson(content: JValue) = {
      val json1 = simplifyArrays(content, List("profile"))
      groupFields(json1, "details", classOf[PersonDetails].getDeclaredFields.map(_.getName).toList)
    }
  }

  override object Genres extends ITMDbGenres {

    override def getList: List[Genre] = {
      val response = httpGetJson(urlFor("Genres.getList"))

      val correctedJson = response transform {
        case JArray(JObject(List(JField("translated", _))) :: tail) => JArray(tail)
      }

      val genreList = correctedJson.extract[List[Genre]]

      // Cache genres for Movie.browse (when using String as genres)
      genreIds = Some(genreList map { g => (g.name, g.id) } toMap)

      genreList
    }
  }

  private def parseMovieFromJson(content: JValue) = {
    val json1 = simplifyArrays(content, List("posters", "backdrops"))
    val json2 = groupFields(json1, "opt", classOf[MovieOptional].getDeclaredFields.map(_.getName).toList.filterNot(_ == "score"), List("score"))
    groupFields(json2, "details", classOf[MovieDetails].getDeclaredFields.map(_.getName).toList)
  }
  private def simplifyArrays(jvalue: JValue, arrayNames: List[String]) = jvalue transform {
    case JField(name, JArray(list)) if arrayNames.contains(name.trim) => {
      val images = for (JObject(List(JField(_, imgContent))) <- list) yield imgContent
      JField(name, JArray(images))
    }
  }
  private def groupFields(jvalue: JValue, targetFieldName: String, fieldNames: List[String], optionalFieldNames: List[String] = Nil) = jvalue transform {
    case JObject(fields) if fieldNames.forall(fieldName => fields.exists(_.name == fieldName)) => {
      val groupedFields = fields filter { field => fieldNames.contains(field.name) || optionalFieldNames.contains(field.name) }
      val otherFields = fields filterNot { field => fieldNames.contains(field.name) || optionalFieldNames.contains(field.name) }
      JObject(JField(targetFieldName, JObject(groupedFields)) :: otherFields)
    }
  }

  private def httpGetJson(url: String): JValue = {
    val response = http.get(url)

    json.parse(response.body) match {
      case parsedJson if (response.statusCode != HttpStatus.SC_OK) => {
        try {
          val error = parsedJson.extract[ErrorDetails]
          throw new TMDbHttpException(response.statusCode, Some(error))
        } catch {
          case e if !e.isInstanceOf[TMDbHttpException] => throw new TMDbHttpException(response.statusCode)
        }
      }
      case JArray(List(JString("Nothing found."))) => JNothing
      case parsedJson => parsedJson
    }
  }

  private def urlFor(method: String) = baseUrl + "/" + apiVersion + "/" + method + "/" + language + "/" + apiType + "/" + apikey
}